package com.xyzwps.lib.openapi.element;

import com.xyzwps.lib.bedrock.UnimplementedException;
import com.xyzwps.lib.json.element.*;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
import java.util.regex.Pattern;

import static com.xyzwps.lib.dollar.Dollar.*;

public class ToYamlVisitor implements OAEVisitor {

    private final List<Line> lines = new ArrayList<>();

    private int indent = 0;

    public String toYaml() {
        var sb = new StringBuilder();
        boolean nextIsItem = false;
        for (var line : lines) {
            if (line == null) {
                nextIsItem = true;
            } else {
                if (nextIsItem) {
                    nextIsItem = false;
                    addIndent(sb, line.indent - 1);
                    sb.append("- ");
                    sb.append(line.text()).append("\n");
                } else {
                    addIndent(sb, line.indent);
                    sb.append(line.text).append("\n");
                }
            }
        }
        return sb.toString();
    }

    private static void addIndent(StringBuilder sb, int indent) {
        if (indent < 0) return;

        sb.append("  ".repeat(indent));
    }

    private static final Pattern NUMBER_PATTERN = Pattern.compile("^[1-9][0-9]*$");

    private static String unescape(String s) {
        if (s == null) {
            return "null";
        }

        if (s.isBlank()) {
            return "'" + s + "'";
        }

        if (NUMBER_PATTERN.matcher(s).matches()) {
            return '\'' + s + "'";
        }

        // TODO: unescape
        return s;
    }

    @Override
    public void visit(Contact contact) {
        lines.add(new Line(indent, "name: " + unescape(contact.name())));

        if (contact.url() != null) {
            lines.add(new Line(indent, "url: " + unescape(contact.url())));
        }

        if (contact.email() != null) {
            lines.add(new Line(indent, "email: " + unescape(contact.email())));
        }
    }

    @Override
    public void visit(OpenApi doc) {
        lines.add(new Line(indent, "openapi: " + doc.openapi()));

        lines.add(new Line(indent, "info:"));
        indent++;
        doc.info().accept(this);
        indent--;

        var servers = doc.servers();
        if (servers != null && !servers.isEmpty()) {
            lines.add(new Line(indent, "servers:"));
            indent++;
            for (var server : servers) {
                lines.add(null);
                server.accept(this);
            }
            indent--;
        }

        var paths = doc.paths();
        if (paths != null) {
            indent++;
            paths.accept(this);
            indent--;
        }

        var tags = doc.tags();
        if (tags != null && !tags.isEmpty()) {
            lines.add(new Line(indent, "tags:"));
            indent++;
            for (var tag : tags) {
                lines.add(null);
                tag.accept(this);
            }
            indent--;
        }

        var externalDocs = doc.externalDocs();
        if (externalDocs != null) {
            lines.add(new Line(indent, "externalDocs:"));
            indent++;
            externalDocs.accept(this);
            indent--;
        }

        var components = doc.components();
        if (!components.isEmpty()) {
            lines.add(new Line(indent, "components:"));
            indent++;
            components.accept(this);
            indent--;
        }
    }

    @Override
    public void visit(Info info) {
        lines.add(new Line(indent, "title: " + unescape(info.title())));

        if (info.summary() != null) {
            lines.add(new Line(indent, "summary: " + unescape(info.summary())));
        }

        if (info.description() != null) {
            lines.add(new Line(indent, "description: " + unescape(info.description())));
        }

        if (info.termsOfService() != null) {
            lines.add(new Line(indent, "termsOfService: " + unescape(info.termsOfService())));
        }

        if (info.contact() != null) {
            lines.add(new Line(indent, "contact:"));
            indent++;
            info.contact().accept(this);
            indent--;
        }

        if (info.license() != null) {
            lines.add(new Line(indent, "license:"));
            indent++;
            info.license().accept(this);
            indent--;
        }

        lines.add(new Line(indent, "version: " + unescape(info.version())));
    }

    @Override
    public void visit(License license) {

        lines.add(new Line(indent, "name: " + unescape(license.name())));

        switch (license) {
            case License.IdLicense idLicense -> {
                if (idLicense.identifier() != null) {
                    lines.add(new Line(indent, "identifier: " + unescape(idLicense.identifier())));
                }
            }
            case License.UrlLicense urlLicense -> {
                if (urlLicense.url() != null) {
                    lines.add(new Line(indent, "url: " + unescape(urlLicense.url())));
                }
            }
        }
    }

    @Override
    public void visit(ExternalDocumentation d) {
        if (d.description() != null) {
            lines.add(new Line(indent, "description: " + unescape(d.description())));
        }

        lines.add(new Line(indent, "url: " + unescape(d.url())));
    }

    @Override
    public void visit(Paths paths) {
        var pathSet = paths.pathSet();
        for (var path : pathSet) {
            var item = paths.item(path);
            if (item != null) {
                lines.add(new Line(indent, path + ":"));
                indent++;
                item.accept(this);
                indent--;
            }
        }
    }

    @Override
    public void visit(Server server) {
        lines.add(new Line(indent, "url: " + unescape(server.url())));

        if (server.description() != null) {
            lines.add(new Line(indent, "description: " + unescape(server.description())));
        }

        var variables = server.variables();
        if (variables != null && !variables.isEmpty()) {
            lines.add(new Line(indent, "variables:"));

            var keySet = new TreeSet<>(variables.keySet());
            for (var key : keySet) {
                indent++;
                lines.add(new Line(indent, key + ":"));
                indent++;
                variables.get(key).accept(this);
                indent -= 2;
            }
        }
    }

    @Override
    public void visit(ServerVariable v) {
        lines.add(new Line(indent, "default: " + unescape(v.defaultValue())));

        var enums = v.enums();
        if (enums != null && !enums.isEmpty()) {
            lines.add(new Line(indent, "enum:"));

            indent += 2;
            for (var e : enums) {
                lines.add(null);
                lines.add(new Line(indent, unescape(e)));
            }
            indent -= 2;
        }

        if (v.description() != null) {
            lines.add(new Line(indent, "description: " + unescape(v.description())));
        }
    }

    @Override
    public void visit(Tag tag) {
        lines.add(new Line(indent, "name: " + unescape(tag.name())));

        if (tag.description() != null) {
            lines.add(new Line(indent, "description: " + unescape(tag.description())));
        }

        if (tag.externalDocs() != null) {
            lines.add(new Line(indent, "externalDocs:"));
            indent++;
            tag.externalDocs().accept(this);
            indent--;
        }
    }

    @Override
    public void visit(Operation op) {
        var tags = op.tags();
        if (tags != null && !tags.isEmpty()) {
            lines.add(new Line(indent, "tags:"));
            indent++;
            for (var tag : tags) {
                lines.add(null);
                lines.add(new Line(indent, tag));
            }
            indent--;
        }

        var summary = op.summary();
        if (summary != null) {
            lines.add(new Line(indent, "summary: " + unescape(summary)));
        }

        var description = op.description();
        if (description != null) {
            lines.add(new Line(indent, "description: " + unescape(description)));
        }

        var externalDocs = op.externalDocs();
        if (externalDocs != null) {
            lines.add(new Line(indent, "externalDocs:"));
            indent++;
            externalDocs.accept(this);
            indent--;
        }

        var operationId = op.operationId();
        if (operationId != null) {
            lines.add(new Line(indent, "operationId: " + unescape(operationId)));
        }

        var deprecated = op.deprecated();
        if (deprecated) {
            lines.add(new Line(indent, "deprecated: true"));
        }

        var parameters = op.parameters();
        if (parameters != null && !parameters.isEmpty()) {
            lines.add(new Line(indent, "parameters:"));
            indent++;
            for (var parameter : parameters) {
                lines.add(null);
                parameter.accept(this);
            }
            indent--;
        }

        var requestBody = op.requestBody();
        if (requestBody != null) {
            lines.add(new Line(indent, "requestBody:"));
            indent++;
            requestBody.accept(this);
            indent--;
        }

        var responses = op.responses();
        if (responses != null) {
            lines.add(new Line(indent, "responses:"));
            indent++;
            responses.accept(this);
            indent--;
        }

        var security = op.security();
        if (security != null && !security.isEmpty()) {
            lines.add(new Line(indent, "security:"));
            indent++;
            for (var requirement : security) {
                lines.add(null);
                requirement.accept(this);
            }
            indent--;
        }

        var servers = op.servers();
        if (servers != null && !servers.isEmpty()) {
            lines.add(new Line(indent, "servers:"));
            indent++;
            for (var server : servers) {
                indent++;
                server.accept(this);
                indent--;
            }
            indent--;
        }
    }

    @Override
    public void visit(PathItem item) {

        var $ref = item.$ref();
        if ($ref != null) {
            lines.add(new Line(indent, "$ref: " + unescape($ref)));
        }

        var summary = item.summary();
        if (summary != null) {
            lines.add(new Line(indent, "summary: " + unescape(summary)));
        }

        var description = item.description();
        if (description != null) {
            lines.add(new Line(indent, "description: " + unescape(description)));
        }

        var get = item.get();
        if (get != null) {
            lines.add(new Line(indent, "get:"));
            indent++;
            get.accept(this);
            indent--;
        }

        var post = item.post();
        if (post != null) {
            lines.add(new Line(indent, "post:"));
            indent++;
            post.accept(this);
            indent--;
        }

        var put = item.put();
        if (put != null) {
            lines.add(new Line(indent, "put:"));
            indent++;
            put.accept(this);
            indent--;
        }

        var delete = item.delete();
        if (delete != null) {
            lines.add(new Line(indent, "delete:"));
            indent++;
            delete.accept(this);
            indent--;
        }

        var head = item.head();
        if (head != null) {
            lines.add(new Line(indent, "head:"));
            indent++;
            head.accept(this);
            indent--;
        }

        var patch = item.patch();
        if (patch != null) {
            lines.add(new Line(indent, "patch:"));
            indent++;
            patch.accept(this);
            indent--;
        }

        var trace = item.trace();
        if (trace != null) {
            lines.add(new Line(indent, "trace:"));
            indent++;
            trace.accept(this);
            indent--;
        }

        var servers = item.servers();
        if (servers != null && !servers.isEmpty()) {
            lines.add(new Line(indent, "servers:"));
            indent++;
            for (var server : servers) {
                lines.add(null);
                server.accept(this);
            }
            indent--;
        }

        var parameters = item.parameters();
        if (parameters != null && !parameters.isEmpty()) {
            lines.add(new Line(indent, "parameters:"));
            indent++;
            for (var p : parameters) {
                lines.add(null);
                p.accept(this);
            }
        }
    }


    @Override
    public void visit(Parameter p) {
        lines.add(new Line(indent, "name: " + unescape(p.name())));
        lines.add(new Line(indent, "in: " + p.in().value));

        var description = p.description();
        if (description != null) {
            lines.add(new Line(indent, "description: " + unescape(description)));
        }

        if (p.required()) {
            lines.add(new Line(indent, "required: true"));
        }

        if (p.deprecated()) {
            lines.add(new Line(indent, "deprecated: true"));
        }

        var schema = p.schema();
        if (schema != null) {
            lines.add(new Line(indent, "schema:"));
            indent++;
            schema.accept(this);
            indent--;
        }

        var style = p.style();
        if (style != null) {
            lines.add(new Line(indent, "style: " + style.value));
        }
    }

    @Override
    public void visit(Reference r) {
        lines.add(new Line(indent, "$ref: '" + r.$ref() + "'"));
    }

    @Override
    public void visit(RequestBody body) {
        var description = body.description();
        if (description != null) {
            lines.add(new Line(indent, "description: " + unescape(description)));
        }

        var content = body.content();
        if (content != null && !content.isEmpty()) {
            lines.add(new Line(indent, "content:"));
            indent++;
            var keySet = new TreeSet<>(content.keySet());
            for (var key : keySet) {
                lines.add(new Line(indent, key + ":"));
                indent++;
                content.get(key).accept(this);
                indent--;
            }
            indent--;
        }

        if (body.required()) {
            lines.add(new Line(indent, "required: true"));
        }
    }

    @Override
    public void visit(Responses response) {
        var statusResponses = response.statusResponses();
        if (statusResponses != null && !statusResponses.isEmpty()) {
            var keySet = new TreeSet<>(statusResponses.keySet());
            for (var key : keySet) {
                lines.add(new Line(indent, "'" + key + "':"));
                indent++;
                statusResponses.get(key).accept(this);
                indent--;
            }
        }

        var defaultResponse = response.responseDefault();
        if (defaultResponse != null) {
            lines.add(new Line(indent, "default:"));
            indent++;
            defaultResponse.accept(this);
            indent--;
        }
    }

    @Override
    public void visit(Response it) {
        lines.add(new Line(indent, "description: " + unescape(it.description())));

        var headers = it.headers();
        if (headers != null && !headers.isEmpty()) {
            lines.add(new Line(indent, "headers:"));
            indent++;
            var keySet = new TreeSet<>(headers.keySet());
            for (var key : keySet) {
                lines.add(new Line(indent, key + ":"));
                indent++;
                headers.get(key).accept(this);
                indent--;
            }
            indent--;
        }

        var content = it.content();
        if (content != null && !content.isEmpty()) {
            lines.add(new Line(indent, "content:"));
            indent++;
            var keySet = new TreeSet<>(content.keySet());
            for (var key : keySet) {
                lines.add(new Line(indent, key + ":"));
                indent++;
                var mediaType = content.get(key);
                mediaType.accept(this);
                indent--;
            }
            indent--;
        }

        var links = it.links();
        if (links != null && !links.isEmpty()) {
            lines.add(new Line(indent, "links:"));
            indent++;
            var keySet = new TreeSet<>(links.keySet());
            for (var key : keySet) {
                lines.add(new Line(indent, key + ":"));
                indent++;
                links.get(key).accept(this);
                indent--;
            }
            indent--;
        }
    }


    @Override
    public void visit(Link link) {
        throw UnimplementedException.todo();
    }

    @Override
    public void visit(Style style) {
        throw UnimplementedException.todo();
    }

    @Override
    public void visit(Header header) {
        throw UnimplementedException.todo();
    }

    @Override
    public void visit(Schema schema) {
        schema.accept(new ToYamlSchemaVisitor(this));
    }

    private class ToYamlSchemaVisitor implements SchemaVisitor {
        private final ToYamlVisitor toYamlVisitor;

        ToYamlSchemaVisitor(ToYamlVisitor visitor) {
            this.toYamlVisitor = visitor;
        }

        @Override
        public void visit(Schema.RefSchema it) {
            lines.add(new Line(indent, "$ref: '" + it.$ref() + "'"));
        }

        @Override
        public void visit(Schema.AllOf it) {
            var title = it.title();
            if ($.isNotBlank(title)) {
                lines.add(new Line(indent, "title: " + unescape(title)));
            }

            var description = it.description();
            if ($.isNotBlank(description)) {
                lines.add(new Line(indent, "description: " + unescape(description)));
            }

            var deprecated = it.deprecated();
            if (deprecated != null && deprecated) {
                lines.add(new Line(indent, "deprecated: true"));
            }

            lines.add(new Line(indent, "allOf:"));
            indent++;
            var schemas = it.schemas();
            for (var s : schemas) {
                lines.add(null);
                s.accept(this);
            }
            indent--;
        }

        @Override
        public void visit(Schema.OneOf it) {
            var title = it.title();
            if ($.isNotBlank(title)) {
                lines.add(new Line(indent, "title: " + unescape(title)));
            }

            var description = it.description();
            if ($.isNotBlank(description)) {
                lines.add(new Line(indent, "description: " + unescape(description)));
            }

            var deprecated = it.deprecated();
            if (deprecated != null && deprecated) {
                lines.add(new Line(indent, "deprecated: true"));
            }

            lines.add(new Line(indent, "oneOf:"));
            indent++;
            var schemas = it.schemas();
            for (var s : schemas) {
                lines.add(null);
                s.accept(this);
            }
            indent--;
        }

        @Override
        public void visit(Schema.AnyOf it) {
            var title = it.title();
            if ($.isNotBlank(title)) {
                lines.add(new Line(indent, "title: " + unescape(title)));
            }

            var description = it.description();
            if ($.isNotBlank(description)) {
                lines.add(new Line(indent, "description: " + unescape(description)));
            }

            var deprecated = it.deprecated();
            if (deprecated != null && deprecated) {
                lines.add(new Line(indent, "deprecated: true"));
            }

            lines.add(new Line(indent, "anyOf:"));
            indent++;
            var schemas = it.schemas();
            for (var s : schemas) {
                lines.add(null);
                s.accept(this);
            }
            indent--;
        }

        @Override
        public void visit(Schema.ObjectSchema it) {
            lines.add(new Line(indent, "type: object"));

            var title = it.title();
            if ($.isNotBlank(title)) {
                lines.add(new Line(indent, "title: " + unescape(title)));
            }

            var description = it.description();
            if ($.isNotBlank(description)) {
                lines.add(new Line(indent, "description: " + unescape(description)));
            }

            var deprecated = it.deprecated();
            if (deprecated != null && deprecated) {
                lines.add(new Line(indent, "deprecated: true"));
            }

            var discriminator = it.discriminator();
            if (discriminator != null) {
                lines.add(new Line(indent, "discriminator:"));
                indent++;
                discriminator.accept(toYamlVisitor);
                indent--;
            }

            var properties = it.properties();
            if (properties != null && !properties.isEmpty()) {
                lines.add(new Line(indent, "properties:"));
                indent++;
                var keySet = new TreeSet<>(properties.keySet());
                for (var key : keySet) {
                    lines.add(new Line(indent, key + ":"));
                    indent++;
                    properties.get(key).accept(this);
                    indent--;
                }
                indent--;
            }

            var required = it.required();
            if (required != null && !required.isEmpty()) {
                lines.add(new Line(indent, "required:"));
                indent++;
                var keySet = new TreeSet<>(required);
                for (var key : keySet) {
                    lines.add(null);
                    lines.add(new Line(indent, key));
                }
                indent--;
            }

            var maxProperties = it.maxProperties();
            if (maxProperties != null) {
                lines.add(new Line(indent, "maxProperties: " + maxProperties));
            }

            var minProperties = it.minProperties();
            if (minProperties != null) {
                lines.add(new Line(indent, "minProperties: " + minProperties));
            }

            var additionalProperties = it.additionalProperties();
            if (additionalProperties != null) {
                lines.add(new Line(indent, "additionalProperties:"));
                indent++;
                additionalProperties.accept(this);
                indent--;
            }

            var example = it.example();
            if (example != null) {
                if (example.isEmpty()) {
                    lines.add(new Line(indent, "example: {}")); // TODO: 感觉不大对
                } else {
                    lines.add(new Line(indent, "example:"));
                    indent++;
                    toYamlVisitor.visit(example);
                    indent--;
                }
            }
        }

        @Override
        public void visit(Schema.ArraySchema it) {
            lines.add(new Line(indent, "type: array"));

            var title = it.title();
            if ($.isNotBlank(title)) {
                lines.add(new Line(indent, "title: " + unescape(title)));
            }

            var description = it.description();
            if ($.isNotBlank(description)) {
                lines.add(new Line(indent, "description: " + unescape(description)));
            }

            var deprecated = it.deprecated();
            if (deprecated != null && deprecated) {
                lines.add(new Line(indent, "deprecated: true"));
            }

            var items = it.items();
            if (items != null) {
                lines.add(new Line(indent, "items:"));
                indent++;
                items.accept(this);
                indent--;
            }

            var example = it.example();
            if (example != null) {
                lines.add(new Line(indent, "example:"));
                indent++;
                toYamlVisitor.visit(example);
                indent--;
            }

            var maxItems = it.maxItems();
            if (maxItems != null) {
                lines.add(new Line(indent, "maxItems: " + maxItems));
            }

            var minItems = it.minItems();
            if (minItems != null) {
                lines.add(new Line(indent, "minItems: " + minItems));
            }

            var uniqueItems = it.uniqueItems();
            if (uniqueItems != null && uniqueItems) {
                lines.add(new Line(indent, "uniqueItems: true"));
            }

            var maxContains = it.maxContains();
            if (maxContains != null) {
                lines.add(new Line(indent, "maxContains: " + maxContains));
            }

            var minContains = it.minContains();
            if (minContains != null) {
                lines.add(new Line(indent, "minContains: " + minContains));
            }
        }

        @Override
        public void visit(Schema.EnumSchema it) {
            lines.add(new Line(indent, "type: string"));

            var title = it.title();
            if ($.isNotBlank(title)) {
                lines.add(new Line(indent, "title: " + unescape(title)));
            }

            var description = it.description();
            if ($.isNotBlank(description)) {
                lines.add(new Line(indent, "description: " + unescape(description)));
            }

            var deprecated = it.deprecated();
            if (deprecated != null && deprecated) {
                lines.add(new Line(indent, "deprecated: true"));
            }

            var example = it.example();
            if (example != null) {
                lines.add(new Line(indent, "example: " + it.example()));
            }

            var enums = it.enums();
            lines.add(new Line(indent, "enum:"));
            indent++;
            for (var e : enums) {
                lines.add(null);
                lines.add(new Line(indent, e));
            }
            indent--;
        }

        @Override
        public void visit(Schema.StringSchema it) {
            lines.add(new Line(indent, "type: string"));

            var title = it.title();
            if ($.isNotBlank(title)) {
                lines.add(new Line(indent, "title: " + unescape(title)));
            }

            var description = it.description();
            if ($.isNotBlank(description)) {
                lines.add(new Line(indent, "description: " + unescape(description)));
            }

            var deprecated = it.deprecated();
            if (deprecated != null && deprecated) {
                lines.add(new Line(indent, "deprecated: true"));
            }

            var format = it.format();
            if (format != null && format.value() != null) {
                lines.add(new Line(indent, "format: " + format.value()));
            }

            var defaultValue = it.defaultValue();
            if ($.isNotBlank(defaultValue)) {
                lines.add(new Line(indent, "default: " + unescape(defaultValue)));
            }

            var example = it.example();
            if (example != null) {
                lines.add(new Line(indent, "example: " + it.example()));
            }

            var maxLength = it.maxLength();
            if (maxLength != null) {
                lines.add(new Line(indent, "maxLength: " + maxLength));
            }

            var minLength = it.minLength();

            if (minLength != null) {
                lines.add(new Line(indent, "minLength: " + minLength));
            }

            var pattern = it.pattern();
            if ($.isNotBlank(pattern)) {
                lines.add(new Line(indent, "pattern: " + unescape(pattern)));
            }
        }

        @Override
        public void visit(Schema.BooleanSchema it) {
            lines.add(new Line(indent, "type: boolean"));

            var title = it.title();
            if ($.isNotBlank(title)) {
                lines.add(new Line(indent, "title: " + unescape(title)));
            }

            var description = it.description();
            if ($.isNotBlank(description)) {
                lines.add(new Line(indent, "description: " + unescape(description)));
            }

            var deprecated = it.deprecated();
            if (deprecated != null && deprecated) {
                lines.add(new Line(indent, "deprecated: true"));
            }
        }

        @Override
        public void visit(Schema.NumberSchema it) {
            lines.add(new Line(indent, "type: number"));

            var title = it.title();
            if ($.isNotBlank(title)) {
                lines.add(new Line(indent, "title: " + unescape(title)));
            }

            var description = it.description();
            if ($.isNotBlank(description)) {
                lines.add(new Line(indent, "description: " + unescape(description)));
            }

            var deprecated = it.deprecated();
            if (deprecated != null && deprecated) {
                lines.add(new Line(indent, "deprecated: true"));
            }

            var defaultValue = it.defaultValue();
            if (defaultValue != null) {
                lines.add(new Line(indent, "default: " + defaultValue));
            }

            var format = it.format();
            if (format != null && format.value() != null) {
                lines.add(new Line(indent, "format: " + format));
            }

            if (it.minimum() != null) {
                lines.add(new Line(indent, "minimum: " + it.minimum()));
            }

            if (it.maximum() != null) {
                lines.add(new Line(indent, "maximum: " + it.maximum()));
            }

            if (it.exclusiveMinimum() != null) {
                lines.add(new Line(indent, "exclusiveMinimum: " + it.exclusiveMinimum()));
            }

            if (it.exclusiveMaximum() != null) {
                lines.add(new Line(indent, "exclusiveMaximum: " + it.exclusiveMaximum()));
            }

            var example = it.example();
            if (example != null) {
                lines.add(new Line(indent, "example: " + it.example()));
            }
        }

        @Override
        public void visit(Schema.IntegerSchema it) {
            lines.add(new Line(indent, "type: integer"));

            var title = it.title();
            if ($.isNotBlank(title)) {
                lines.add(new Line(indent, "title: " + unescape(title)));
            }

            var description = it.description();
            if ($.isNotBlank(description)) {
                lines.add(new Line(indent, "description: " + unescape(description)));
            }

            var deprecated = it.deprecated();
            if (deprecated != null && deprecated) {
                lines.add(new Line(indent, "deprecated: true"));
            }

            var defaultValue = it.defaultValue();
            if (defaultValue != null) {
                lines.add(new Line(indent, "default: " + defaultValue));
            }

            var format = it.format();
            if (format != null && format.value() != null) {
                lines.add(new Line(indent, "format: " + format.value()));
            }

            if (it.minimum() != null) {
                lines.add(new Line(indent, "minimum: " + it.minimum()));
            }

            if (it.maximum() != null) {
                lines.add(new Line(indent, "maximum: " + it.maximum()));
            }

            if (it.exclusiveMinimum() != null) {
                lines.add(new Line(indent, "exclusiveMinimum: " + it.exclusiveMinimum()));
            }

            if (it.exclusiveMaximum() != null) {
                lines.add(new Line(indent, "exclusiveMaximum: " + it.exclusiveMaximum()));
            }

            var example = it.example();
            if (example != null) {
                lines.add(new Line(indent, "example: " + it.example()));
            }
        }
    }


    // TODO: 改用 JsonElement 的 visitor
    private void visit(JsonObject jo) {
        var keySet = new TreeSet<>(jo.keySet());
        for (var key : keySet) {
            switch (jo.get(key)) {
                case JsonObject j0 -> {
                    if (jo.isEmpty()) {
                        lines.add(new Line(indent, key + ": {}"));
                    } else {
                        lines.add(new Line(indent, key + ":"));
                        indent++;
                        this.visit(j0);
                        indent--;
                    }
                }
                case JsonArray ja -> {
                    if (ja.isEmpty()) {
                        lines.add(new Line(indent, key + ": []"));
                    } else {
                        lines.add(new Line(indent, key + ":"));
                        indent++;
                        this.visit(ja);
                        indent--;
                    }
                }
                case JsonString js -> lines.add(new Line(indent, key + ": " + unescape(js.value())));
                case JsonDecimal jd -> lines.add(new Line(indent, key + ": " + jd.value()));
                case JsonInteger ji -> lines.add(new Line(indent, key + ": " + ji.value()));
                case JsonBoolean jb -> lines.add(new Line(indent, key + ": " + jb.value));
                case JsonNull jn -> lines.add(new Line(indent, key + ": null"));
                case null -> lines.add(new Line(indent, key + ": null"));
            }
        }
    }

    private void visit(JsonArray ja) {
        for (var e : ja) {
            lines.add(null);
            switch (e) {
                case JsonObject j0 -> {
                    if (j0.isEmpty()) {
                        lines.add(new Line(indent, "{}"));
                    } else {
                        indent++;
                        this.visit(j0);
                        indent--;
                    }
                }
                case JsonArray j -> {
                    if (j.isEmpty()) {
                        lines.add(new Line(indent, "[]"));
                    } else {
                        indent++;
                        this.visit(j);
                        indent--;
                    }
                }
                case JsonString js -> lines.add(new Line(indent, unescape(js.value())));
                case JsonDecimal jd -> lines.add(new Line(indent, "" + jd.value()));
                case JsonInteger ji -> lines.add(new Line(indent, "" + ji.value()));
                case JsonBoolean jb -> lines.add(new Line(indent, "" + jb.value));
                case JsonNull jn -> lines.add(new Line(indent, "null"));
                case null -> lines.add(new Line(indent, "null"));
            }
        }
    }

    @Override
    public void visit(Example example) {
        var summary = example.summary();
        if (summary != null) {
            lines.add(new Line(indent, "summary: " + unescape(summary)));
        }

        var description = example.description();
        if (description != null) {
            lines.add(new Line(indent, "description: " + unescape(description)));
        }

        var value = example.value();
        if (value != null) {
            switch (value) {
                case String s -> lines.add(new Line(indent, "value: " + unescape(s)));
                case Map x -> {
                    Map<String, String> m = (Map<String, String>) x;
                    lines.add(new Line(indent, "value:"));
                    indent++;
                    var keySet = new TreeSet<>(m.keySet());
                    for (var key : keySet) {
                        var v = m.get(key);
                        lines.add(new Line(indent, key + ": " + unescape(v)));
                    }
                    indent--;
                }
                default -> throw new IllegalStateException("Unsupported value type: " +
                        value.getClass().getCanonicalName());
            }
        }

        var externalValue = example.externalValue();
        if (externalValue != null) {
            lines.add(new Line(indent, "externalValue: " + unescape(externalValue)));
        }
    }

    @Override
    public void visit(Encoding encoding) {
        throw UnimplementedException.todo();
    }

    @Override
    public void visit(MediaType it) {
        boolean empty = true;

        var schema = it.schema();
        if (schema != null) {
            empty = false;
            lines.add(new Line(indent, "schema:"));
            indent++;
            schema.accept(this);
            indent--;
        }

        var example = it.example();
        if (example != null) {
            empty = false;
            lines.add(new Line(indent, "example: " + unescape(example)));
        }

        var examples = it.examples();
        if (examples != null && !examples.isEmpty()) {
            empty = false;
            lines.add(new Line(indent, "examples:"));
            indent++;
            var keySet = new TreeSet<>(examples.keySet());
            for (var key : keySet) {
                lines.add(new Line(indent, key + ":"));
                indent++;
                examples.get(key).accept(this);
                indent--;
            }
            indent--;
        }

        var encoding = it.encoding();
        if (encoding != null && !encoding.isEmpty()) {
            empty = false;
            lines.add(new Line(indent, "encoding:"));
            indent++;
            var keySet = new TreeSet<>(encoding.keySet());
            for (var key : keySet) {
                lines.add(new Line(indent, key + ":"));
                indent++;
                var enc = encoding.get(key);
                enc.accept(this);
                indent--;
            }
            indent--;
        }

        if (empty) {
            if (lines.isEmpty()) {
                lines.add(new Line(indent, "{}"));
            } else {
                lines.getLast().append(" {}");
            }
        }
    }

    @Override
    public void visit(SecurityRequirement requirement) {
        var requirements = requirement.requirements();
        if (requirements != null && !requirements.isEmpty()) {
            var keySet = new TreeSet<>(requirements.keySet());
            for (var key : keySet) {
                var scopes = requirements.get(key);
                if (scopes != null && !scopes.isEmpty()) {
                    lines.add(new Line(indent, key + ":"));
                    for (var scope : scopes) {
                        lines.add(new Line(indent, "- " + scope));
                    }
                }
            }
        } else {
            lines.add(new Line(indent, "{}"));
        }
    }

    @Override
    public void visit(Components components) {
        var schemas = components.schemas();
        if (schemas != null && !schemas.isEmpty()) {
            lines.add(new Line(indent, "schemas:"));
            indent++;
            var keySet = new TreeSet<>(schemas.keySet());
            for (var key : keySet) {
                lines.add(new Line(indent, key + ":"));
                indent++;
                schemas.get(key).accept(this);
                indent--;
            }
            indent--;
        }
    }

    @Override
    public void visit(Discriminator d) {
        lines.add(new Line(indent, "propertyName: " + unescape(d.propertyName())));

        var mapping = d.mapping();
        if (mapping != null && !mapping.isEmpty()) {
            lines.add(new Line(indent, "mapping:"));
            indent++;
            var keySet = new TreeSet<>(mapping.keySet());
            for (var key : keySet) {
                lines.add(new Line(indent, key + ": " + unescape(mapping.get(key))));
            }
            indent--;
        }
    }

    private final class Line {

        private final int indent;
        private String text;

        public Line(int indent, String text) {
            this.indent = indent;
            this.text = text;
        }

        public int indent() {
            return indent;
        }

        public String text() {
            return text;
        }

        public void append(String s) {
            this.text += s;
        }
    }
}
